Jerry's Log

Spring DI

contents

오늘은 스프링에서 빈에 의존성을 주입하는 3가지 방식을 비교해보겠습니다.

세 가지 방식 모두 '빈에 의존성을 넣는다'는 결과는 같지만, 설계의 품질, 테스트 용이성, 불변성(Immutability) 측면에서 엄청난 차이가 있습니다.


1. 필드 주입 (Field Injection) - "안티 패턴"

초보자들이 가장 많이 쓰는 방식입니다. 코드가 간결하기 때문입니다. 필드 위에 @Autowired만 붙이면 끝납니다.

@Service
public class UserService {
    @Autowired // ⚠️ 필드 주입 사용
    private UserRepository userRepository;

    public void register() {
        userRepository.save(new User());
    }
}

장점

단점 및 문제점

  1. 불변성 위반: 필드를 final로 선언할 수 없습니다. 즉, 객체가 생성된 후에 userRepository가 실수로 변경될 가능성이 열려 있습니다.
  2. Spring과 강한 결합: 이 클래스는 스프링 컨테이너 없이는 무용지물입니다. 필드가 private이고 세터(Setter)나 생성자가 없으므로, 순수 자바 코드로 new UserService()를 해서 테스트하려 해도 userRepository에 값을 넣어줄 방법이 없습니다.
  3. 테스트의 악몽: 단위 테스트(Unit Test)를 작성하려면 리플렉션(Reflection) 을 쓰거나, 메서드 하나 테스트하자고 무거운 스프링 컨텍스트(@SpringBootTest)를 전부 띄워야 합니다. 단순히 Mock 객체를 넣어줄 수가 없습니다.
  4. 의존성 숨김: 만약 클래스가 10개의 의존성을 가진다면, 생성자 주입은 생성자가 너무 길어져서 "이 클래스가 너무 많은 일을 하는구나(단일 책임 원칙 위반)"를 바로 알 수 있습니다. 하지만 필드 주입은 이를 숨기기 때문에 코드를 다 훑어보기 전에는 복잡도를 파악하기 어렵습니다.

2. 수정자 주입 (Setter Injection) - "선택적 의존성"

public 세터 메서드를 통해 의존성을 주입합니다.

@Service
public class UserService {
    private UserRepository userRepository;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

장점

단점 및 문제점

  1. 불완전한 초기화: 세터를 호출하지 않고도 UserService 객체를 생성할 수 있습니다. 그 상태에서 register()를 호출하면 NullPointerException이 발생합니다. 객체가 불안정한 상태로 만들어질 수 있습니다.
  2. 불변성 없음: 필드 주입과 마찬가지로 final을 사용할 수 없습니다.
  3. 번거로움: 세터 메서드를 일일이 작성해야 해서 코드가 길어집니다.

3. 생성자 주입 (Constructor Injection) - "표준(Gold Standard)"

Spring 4.3 이후부터 강력하게 권장되는 방식입니다. 생성자를 통해 의존성을 전달받습니다.

@Service
public class UserService {
    private final UserRepository userRepository; // ✅ Final 사용 가능!

    // 생성자가 하나만 있으면 @Autowired 생략 가능
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

장점

// 스프링 없이 순수 자바 단위 테스트 가능!
UserRepository mockRepo = Mockito.mock(UserRepository.class);
UserService service = new UserService(mockRepo); // 그냥 Mock을 넣어주면 끝

단점

현대적인 방식 (Lombok 활용):

@Service
@RequiredArgsConstructor // ✅ 모든 'final' 필드에 대한 생성자를 자동 생성
public class UserService {
    private final UserRepository userRepository;
}

4. 심층 분석: "순환 참조(Circular Dependency)" 문제

이것이 기술적으로 가장 큰 차이점입니다.

시나리오: Bean A가 Bean B를 필요로 하고, Bean B가 Bean A를 필요로 하는 상황.

Case A: 필드/수정자 주입 사용 시

스프링은 Bean A 생성 Bean B 필요함 확인 Bean B 생성 Bean A 필요함 확인.

Case B: 생성자 주입 사용 시

스프링이 Bean A 생성 시도 생성자가 Bean B 요구 Bean B 생성 시도 생성자가 Bean A 요구.

5. 요약 테이블

특징 필드 주입 (Field) 수정자 주입 (Setter) 생성자 주입 (Constructor)
가독성 높음 (가장 깔끔) 낮음 (장황함) 높음 (Lombok 사용 시)
불변성 (final) ❌ 불가 ❌ 불가 가능
신뢰성 ⚠️ 낮음 (NPE 위험) ⚠️ 낮음 (NPE 위험) 높음 (컴파일 타임 안전)
테스트 용이성 ❌ 어려움 (리플렉션 필요) ⚠️ 보통 쉬움 (POJO)
순환 참조 처리 숨겨버림 (나쁨) 숨겨버림 (나쁨) 즉시 실패 (좋음)
스프링 권고 비권장 / 지양 선택적 의존성에만 사용 강력 추천

주니어 개발자를 위한 제언

무조건 생성자 주입을 사용하세요. (특히 Lombok의 @RequiredArgsConstructor와 함께)

  1. NullPointerException을 예방합니다.
  2. 단위 테스트 작성이 훨씬 쉬워집니다.
  3. IntelliJ IDEA도 필드 주입을 쓰면 "Field injection is not recommended"라고 경고를 띄웁니다.

references